package de.schnitzeljaeger.model.dao.impl;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

public class DatabaseUtil extends SQLiteOpenHelper {
	
	static private final String LOG_TAG = "schnitzeljaeger";
	static private final String DATABASE_NAME = "paperchase";
	static private final int DATABASE_VERSION = 8;
	static private final String TABLE_PAPER_CHASE = "paper_chase";
	static private final String DATABASE_CREATE = "CREATE TABLE " + TABLE_PAPER_CHASE + "(" +
												"_id INTEGER PRIMARY KEY AUTOINCREMENT," +
												"name VARCHAR(100) NOT NULL," +
												"description VARCHAR(255)," +
												"published INTEGER(1) NOT NULL)";
	
	static private final char T_UNKNOWN		= (char) 0;
	static private final char T_STRING		= (char) 1;
	static private final char T_LONG		= (char) 2;
	static private final char T_INTEGER		= (char) 3;	
	static private final char T_SHORT		= (char) 4;
	static private final char T_BYTE		= (char) 5;
	static private final char T_BOOLEAN		= (char) 6;
	static private final char T_FLOAT		= (char) 7;
	static private final char T_DOUBLE		= (char) 8;
	static private final char T_DATE		= (char) 9;
	static private final char T_BYTE_ARRAY	= (char) 10;
	
	static private final Map<Class<?>, Character> primitiveTypes = new HashMap<Class<?>, Character>();
	
	static {
		primitiveTypes.put(String.class,	T_STRING);
		primitiveTypes.put(Long.class,		T_LONG);
		primitiveTypes.put(long.class,		T_LONG);
		primitiveTypes.put(Integer.class,	T_INTEGER);
		primitiveTypes.put(int.class,		T_INTEGER);
		primitiveTypes.put(Short.class,		T_SHORT);
		primitiveTypes.put(short.class,		T_SHORT);
		primitiveTypes.put(Byte.class,		T_BYTE);
		primitiveTypes.put(byte.class,		T_BYTE);
		primitiveTypes.put(Boolean.class,	T_BOOLEAN);
		primitiveTypes.put(boolean.class,	T_BOOLEAN);
		primitiveTypes.put(Float.class,		T_FLOAT);
		primitiveTypes.put(float.class,		T_FLOAT);
		primitiveTypes.put(Double.class,	T_DOUBLE);
		primitiveTypes.put(double.class,	T_DOUBLE);
		primitiveTypes.put(Date.class,		T_DATE);
		primitiveTypes.put(byte[].class,	T_BYTE_ARRAY);
	}
	
	
	public DatabaseUtil(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }
	
    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(DATABASE_CREATE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    	final String infoMsg = String.format("Upgrading database from version %d to %d" +
        					", which will destroy all old data", oldVersion, newVersion);
    	final String sql = "DROP TABLE IF EXISTS " + TABLE_PAPER_CHASE;
    	
        Log.w(LOG_TAG, infoMsg);
        db.execSQL(sql);
        onCreate(db);
    }
    
    @Override
    public SQLiteDatabase getWritableDatabase() {
    	return super.getWritableDatabase();
    }
    
    public <T> void entityToValues(T entity, Class<T> specificInterface, ContentValues values) {
    	Class<?> entityClass = entity.getClass();
    	
    	if (entityClass == specificInterface) {
    		entityToValues(entity, values);
    		return;
    	}
    	
    	Class<?>[] superInterfaces = specificInterface.getInterfaces();
    	
    	internalEntityToValues(entityClass, specificInterface, entity, values);
    	
    	for (Class<?> superInterface : superInterfaces)
    		internalEntityToValues(entityClass, superInterface, entity, values);
    }
    
    private <T> void internalEntityToValues(Class<?> entityClass, Class<?> specificInterface, T entity, ContentValues values) {
    	for (Method m : specificInterface.getDeclaredMethods()) {
			final String methodName = m.getName();
			
			if (!methodName.startsWith("set"))
				continue;

			final String fldName = methodName.substring(3, 4).toLowerCase() + methodName.substring(4);
			
			try {
				Field fld = entityClass.getDeclaredField(fldName);
				final int modifiers = fld.getModifiers();
			
				if (!Modifier.isStatic(modifiers) && !Modifier.isTransient(modifiers) &&
						primitiveTypes.get(fld.getType()) != null)
					internalEntityToValue(values, fld, entity);
    		} catch (SecurityException e1) {
    			throw new RuntimeException(String.format("No permission to access field %s of %s", fldName, specificInterface), e1);
    		} catch (NoSuchFieldException e2) { }
		}
    }
    
    public void entityToValues(Object entity, ContentValues values) {
		Class<?> objClass = entity.getClass();
		
		while (objClass != null) {
			for (Field fld : objClass.getDeclaredFields()) {
				final int modifiers = fld.getModifiers();
				
				if (!Modifier.isStatic(modifiers) && !Modifier.isTransient(modifiers) && !fld.getName().equals("id"))
					internalEntityToValue(values, fld, entity);
			}

			objClass = objClass.getSuperclass();
		}
	}
    
    private void internalEntityToValue(ContentValues values, Field fld, Object entity) {
    	String column = toSqlName(fld.getName());
    	Character fldType = primitiveTypes.get(fld.getType());
	
    	fld.setAccessible(true);
    	
		try {
			if (fldType == null)
				return;
				
			if (fld.get(entity) == null)
				values.putNull(column);
			else {
				switch(fldType) {
				case T_STRING:
					values.put(column, (String) fld.get(entity));
					break;
				case T_LONG:
					values.put(column, (Long) fld.get(entity));
					break;
				case T_INTEGER:
					values.put(column, (Integer) fld.get(entity));
					break;
				case T_SHORT:
					values.put(column, (Short) fld.get(entity));
					break;
				case T_BYTE:
					values.put(column, (Byte) fld.get(entity));
					break;
				case T_BOOLEAN:
					values.put(column, (Boolean) fld.get(entity) ? 1 : 0);
					break;
				case T_FLOAT:
					values.put(column, (Float) fld.get(entity));
					break;
				case T_DOUBLE:
					values.put(column, (Double) fld.get(entity));
					break;
				case T_DATE:
					values.put(column, ((Date) fld.get(entity)).getTime());
					break;
				case T_BYTE_ARRAY:
					values.put(column, (byte[]) fld.get(entity));
					break;
				}
			}
		} catch (IllegalArgumentException e) {
			throw new RuntimeException("Wrong property owner given", e);
		} catch (IllegalAccessException e) {
			throw new RuntimeException(String.format("Cannot access field %s of %s", fld.getName(), entity.getClass()), e);
		}
    }
	
    public void valuesToEntity(Cursor cursor, Object entity) {
		String[] colNames = cursor.getColumnNames();
		
		for (int i = 0; i < colNames.length; i++)
			valueToEntity(cursor, i, colNames[i], entity);
	}
    
    private void valueToEntity(Cursor cursor, int i, String column, Object entity) {
    	Class<?> entityClass = entity.getClass();
		final String fldName = toJavaName(column);
		
		try {
			Field fld = entityClass.getDeclaredField(fldName);
			fld.setAccessible(true);
			Character fldType = primitiveTypes.get(fld.getType());
			Object value = null;
			
			if (fldType == null)
				fldType = T_UNKNOWN;
			
			if (!cursor.isNull(i)) {
				switch(fldType) {
				case T_STRING:
					value = cursor.getString(i);
					break;
				case T_LONG:
					value = cursor.getLong(i);
					break;
				case T_INTEGER:
					value = cursor.getInt(i);
					break;
				case T_SHORT:
					value = cursor.getShort(i);
					break;
				case T_BYTE:
					value = (byte) cursor.getInt(i);
					break;
				case T_BOOLEAN:
					value = cursor.getInt(i) != 0;
					break;
				case T_FLOAT:
					value = cursor.getFloat(i);
					break;
				case T_DOUBLE:
					value = cursor.getDouble(i);
					break;
				case T_DATE:
					value = new Date(cursor.getLong(i));
					break;
				case T_BYTE_ARRAY:
					value = cursor.getBlob(i);
					break;
				default:
					throw new UnsupportedOperationException(String.format("Unsupported field type %s of field %s of %s", fldType, fldName, entityClass));
				}
			}
		
			fld.set(entity, value);
		} catch (SecurityException e1) {
			throw new RuntimeException(String.format("No permission to access field %s of %s", fldName, entityClass), e1);
		} catch (NoSuchFieldException e2) {
			throw new RuntimeException(String.format("Unknown field name %s of ", fldName, entityClass), e2);
		} catch (IllegalArgumentException e3) {
			throw new RuntimeException("Wrong property owner given", e3);
		} catch (IllegalAccessException e4) {
			throw new RuntimeException(String.format("Cannot access field %s of %s", fldName, entityClass), e4);
		}
    }
    
    public String[] retrievePropertyNames(Class<?> entityClass) {
    	List<String> result = new LinkedList<String>();
		
		while (entityClass != null) {
			for (Field fld : entityClass.getDeclaredFields()) {
				final String fldName = fld.getName();
				final int modifiers = fld.getModifiers();
				
				if (!Modifier.isStatic(modifiers) && !Modifier.isTransient(modifiers)) {
					String column = toSqlName(fldName);
					Character fldType = primitiveTypes.get(fld.getType());
					
					if (fldType != null)
						result.add(column);
				}
			}
			
			entityClass = entityClass.getSuperclass();
		}
		
		return result.toArray(new String[result.size()]);
	}
    
    public String[] retrievePropertyNames(Class<?> entityClass, Class<?> specificInterface) {
    	if (entityClass == specificInterface)
    		return retrievePropertyNames(entityClass);
    	
    	List<String> result = new LinkedList<String>();
		
    	Class<?>[] superInterfaces = specificInterface.getInterfaces();
    	
    	internalRetrievePropertyNames(entityClass, specificInterface, result);
    	
    	for (Class<?> superInterface : superInterfaces)
    		internalRetrievePropertyNames(entityClass, superInterface, result);
		
		return result.toArray(new String[result.size()]);
	}
    
    private void internalRetrievePropertyNames(Class<?> entityClass, Class<?> specificInterface, Collection<String> result) {
    	for (Method m : specificInterface.getDeclaredMethods()) {
			final String methodName = m.getName();
			int prfxLength = 0;
			
			if (methodName.startsWith("get"))
				prfxLength = 3;
			else if (methodName.startsWith("is"))
				prfxLength = 2;
			else
				continue;
			
			final int firstChar = prfxLength + 1;
			final String fldName = methodName.substring(prfxLength, firstChar).toLowerCase() +
									methodName.substring(firstChar);
			
			try {
				Field fld = entityClass.getDeclaredField(fldName);
				final int modifiers = fld.getModifiers();
			
				if (!Modifier.isStatic(modifiers) && !Modifier.isTransient(modifiers) &&
						primitiveTypes.get(fld.getType()) != null) {
					final String column = toSqlName(fldName);
					
					result.add(column);
				}
			} catch (SecurityException e) {
				throw new RuntimeException(String.format("No permission to access field %s of %s", fldName, specificInterface), e);
			} catch (NoSuchFieldException e) {
			}
		}
    }
						
    public String toSqlName(final String javaName) {
    	if (javaName.equals("id"))
    		return "_id";
    	
		final StringBuilder sb = new StringBuilder();
		final int length = javaName.length();
		
		for (int i = 0; i < length; i++) {
			char c = javaName.charAt(i);
			
			if (Character.isUpperCase(c))
				sb.append('_').append(Character.toLowerCase(c));
			else
				sb.append(c);
		}
		
		return sb.toString();
	}
	
    public String toJavaName(final String sqlName) {
    	if (sqlName.equals("_id"))
    		return "id";
    	
    	final StringBuilder sb = new StringBuilder();
		final int length = sqlName.length();
		
		for (int i = 0; i < length; i++) {
			char c = sqlName.charAt(i);
			
			if (c == '_')
				sb.append(Character.toUpperCase(sqlName.charAt(++i)));
			else
				sb.append(c);
		}
		
		return sb.toString();
	}
}
